Persisting Domain Objects
Map a DDD domain model to a relational database using Spring Data JPA, Hibernate, H2, and Lombok.
Overview
In Activity 02, you designed the domain model of a Library Management System using Domain-Driven Design (DDD). You identified entities, value objects, aggregate roots, bounded contexts, and relationships between domain concepts.
In this activity, we take the next step: we persist the domain model.
We will deliberately postpone inserting application data. Data initialization, CRUD operations, and repository usage will be covered in Activity 04.
Learning Goals
By the end of this activity, you should be able to:
- Explain the relationship between a DDD domain model and a relational database.
- Map a domain entity to a JPA entity using
@Entity. - Identify an entity's database identity using
@Id. - Map Value Objects using
@Embeddableand@Embedded. - Map Java enumerations using
@Enumerated(EnumType.STRING). - Explain why Lombok can reduce boilerplate code.
- Explain why
@Datashould not automatically be used on JPA entities. - Understand the role of Spring Data JPA repositories.
- Verify that Hibernate has created database tables using the H2 Console.
Before You Begin
You should already have:
- Completed Activity 02.
- The Library Management System Spring Boot project.
- Java and Gradle configured correctly.
- Spring Boot running successfully.
Deliverables
Before submitting the activity, make sure your project contains:
- The three bounded-context package structures.
- Member Management JPA entities and Value Objects.
- Spring Data JPA repositories.
- Appropriate Lombok annotations.
- A successfully running Spring Boot application.
- Database tables visible in the H2 Console.
1. From Domain Model to Database
A DDD model describes the business concepts of an application. A relational database stores data in tables, rows, and columns.
JPA provides a way to connect these two worlds. Hibernate is the JPA implementation that will perform the actual object-relational mapping.
DDD Domain Model | | JPA annotations v Java Objects | | Hibernate v Relational Database | v H2 Tables
The important idea is that JPA maps the objects; it does not design your domain model for you.
2. Organizing the Project by Bounded Context
In Activity 02, we separated the Library Management System into different bounded contexts. We will continue this organization in the Java project. Each subdomain gets its own package, and each package contains separate layers.
Thus your first task is to create the necessary packages and structure for each bounded context. You can do that inside an IED like IntelliJ or Eclipse, or you can create the packages manually in the file system. i.e. using the file explorer or command line. The important thing is to create the packages in the correct location, and with the correct names.
Project Package Structure
src/main/java/com/example/library
____________________________________________________________________________________
| | |
|
membermanagement
│
├── domainlayer
│ ├── entity
│ ├── valueobject
│ └──
│
├── infrastructurelayer
│ └── repository
│
├── businesslogiclayer
│ └── services
│
└── presentationlayer
└── controllers |
loanmanagement │ ├── domainlayer │ ├── entity │ ├── valueobject │ └── │ ├── infrastructurelayer │ └── repository │ ├── businesslogiclayer │ └── services │ └── presentationlayer └── controllers |
collectionmanagement │ ├── domainlayer │ ├── entity │ ├── valueobject │ └── repository │ ├── infrastructurelayer │ └── repository │ ├── businesslogiclayer │ └── services │ └── presentationlayer └── controllers |
Part 1 : Reducing Boilerplate with Lombok
Java classes often contain a large amount of repetitive code:
getters, setters, constructors, toString(),
equals(), and hashCode().
Lombok is a library that can generates much of this code automatically during compilation, so you don't have to write it yourself. The following example shows a simple class with and without Lombok, so you can see the difference.
Without Lombok
public class Member {
private String name;
private String email;
public Member() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
With Lombok
@Getter
@Setter
@NoArgsConstructor
public class Member {
private String name;
private String email;
}
You can see that the first version is longer and contains a lot of boilerplate code. The second version is shorter and easier to read. It also saves you time and reduces the risk of errors. Let us understand the meaning of the annotations used in the second version of the class.
Important Lombok Annotations
| Annotation | Purpose |
|---|---|
@Getter |
Generates getter methods. |
@Setter |
Generates setter methods. |
@NoArgsConstructor |
Generates a no-argument constructor. |
@RequiredArgsConstructor |
Generates a constructor for required fields. |
@AllArgsConstructor |
Generates a constructor containing all fields. |
@Builder |
Provides the Builder pattern. |
@ToString |
Generates toString(). |
@EqualsAndHashCode |
Generates equality methods. |
@Data |
Combines several Lombok features. |
All these annotations are optional. You can use only the ones you need.
For example, if you only need getters and setters, you can use only @Getter and @Setter.
If you need a no-argument constructor, you can use @NoArgsConstructor.
If you need a constructor with required fields, you can use @RequiredArgsConstructor.
If you need a constructor with all fields, you can use @AllArgsConstructor.
and so on. You can mix and match these annotations as needed. But one thing to keep in mind is that they all must be
inserted
Note that the annotations are processed at compile time, so you will not see the generated code in your source files. But you can see the generated code in the compiled class files. You can also use your IDE to view the generated code. For example, in IntelliJ IDEA, you can browse the build directory and look at the generated files.
@Data on JPA entities.
@Data generates getters, setters,
equals(), hashCode(), and
toString().
These generated methods are not always appropriate for
persistent entities.
Thus, we should not simply add @Data to every class.
In this course we will use Lombok selectively. For JPA entities, a common pattern will be:
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Data when appropriate because
DTOs generally have simpler lifecycle and equality requirements. We will talk more about DTOs in later activities.
Step 1 : Verify Lombok
In order to be able to use Lombok, you need to add it as a dependency to your project,
in the build.gradle file (if you are using Gradle).
Make sure you add the following dependency to your build.gradle file:
implementation 'org.projectlombok:lombok:1.18.20'
annotationProcessor 'org.projectlombok:lombok:1.18.20'
Quick Check
Look at one of your classes that uses Lombok. Can you identify which methods are generated automatically?
Part 2: Turning an Entity into a JPA Entity
A DDD entity has an identity. A JPA entity also needs an identity so Hibernate can identify the corresponding database row.
Consider the Member aggregate root from Activity 02.
public class Member {
private UUID memberId;
private MemberName name;
private Email email;
private MembershipTier tier;
private MemberStatus status;
private int activeLoanCount;
private LocalDate registrationDate;
}
To make this class persistent, we add JPA annotations.
@Entity
@Table(name = "members")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
@Embedded
private MemberId memberLibraryId;
@Embedded
private MemberName name;
@Column(unique = true)
@Embedded
private EmailAddress email;
@Min(value = 0, message = "Active loan count cannot be negative")
private int activeLoanCount;
@Enumerated(EnumType.STRING)
private MemberStatus memberStatus;
@Enumerated(EnumType.STRING)
private MembershipTier membershipTier;
private LocalDate registrationDate;
// getters, setters are geneated by Lombok
public Member(
MemberId memberLibraryId,
MemberName name,
EmailAddress email,
MembershipTier tier
) {
this.memberLibraryId = memberLibraryId;
this.name = name;
this.email = email;
this.membershipTier = tier;
this.memberStatus = MemberStatus.ACTIVE;
this.activeLoanCount = 0;
this.registrationDate = LocalDate.now();
}
}
Read the Code
| Code | Meaning |
|---|---|
@Entity |
Marks the class as a JPA persistent entity. |
@Table(name = "members") |
Specifies the database table name. |
@Id |
Identifies the primary key. |
@Embedded |
Embeds a Value Object into the entity table. |
@Enumerated(EnumType.STRING) |
Stores the enum value as text. |
@Getter |
Lombok generates getter methods. |
@NoArgsConstructor(...) |
Provides the constructor required by JPA. |
@Setter on the entity.
We do not want every field to become freely mutable (meaning we do not want to allow any field to be changed at any time by any part of the system).
Domain behavior should control how important state changes. Meaning that the entity should provide methods that encapsulate the business rules for changing its state,
rather than exposing setters for all fields.
in order to use these annotations you need to add the necessary dependency in build.gradle:
implementation 'org.projectlombok:lombok:1.18.20'
annotationProcessor 'org.projectlombok:lombok:1.18.20'
implementation 'jakarta.persistence:jakarta.persistence-api:3.2.0'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
Part 3: Persisting Value Objects
In Activity 02, we identified concepts such as MemberId, MemberName, and
EmailAddress and as Value Objects. see the diagram below. Value objects are immutable and do not have their own identity.
thus, value objects should be implemented as immutable classes, meaning that their state cannot be changed after they are created.
Here how we can implement the id and EmailAddress (We cannot use Email as the name of the class because
it would conflict with @Email annotation from javax.validation.constraints package)
value object as an immutable class:
Value Object implementation
Here is the code for the value objects: EmailAddress.
@Embeddable
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class EmailAddress {
@Column(name = "email", nullable = false, unique = true)
@Email
private String value;
// getters, setters are geneated by Lombok
}
The EmailAddress object does not need its own database table.
Its value can be stored directly inside the members table (i.e. embedded inside that table).
JPA provides a way to embed a Value Object inside an Entity using the @Embeddable annotation on the Value Object class
and the @Embedded annotation on the Entity class.
@Embedded
private EmailAddress email;
Conceptually:
Member │ ├── memberId ├── name ├── email ────────────────┐ ├── tier │ ├── status │ └── registrationDate │ │ v members table member_id first_name last_name email tier status active_loan_count registration_date
Here is the code for the value objects: MemberId and MemberName.
@Embeddable
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class MemberName {
@NotBlank(message = "First name is required")
private String firstName;
@NotBlank(message = "Last name is required")
private String lastName;
}
@Embeddable
@Getter
@Setter
@AllArgsConstructor
public class MemberId {
private UUID libraryId;
public MemberId(){
this.libraryId = UUID.randomUUID();
}
}
One could ask: what is the use of having the MemberId value object if it is just a wrapper around a UUID? The answer is that it provides a clear and explicit representation of the concept of a MemberId in the domain model. It allows us to encapsulate any validation or business rules related to MemberId in one place, and it makes the code more readable and expressive. For example, in library management context, the member ID is created based on some criteria/business rules set by the Library. This is the place where the MemberId value object shines. The code related to its creation and validation is centralized in one location.
Another question is why define id and memberId attributes in the Member entity? isn't that redundant? the answer is that the id attribute is used by JPA/Hibernate to identify the entity in the database (i.e. the primary key of the members table), while the memberId attribute is used by the domain model (which the library domain) to represent the concept of a member's unique identifier. One is an internal technical identifier used by the persistence layer, while the other is an external identifier used by the domain layer to represent a domain concept that has meaning in the business context.
Part 4 : Persisting Enumerations
Our domain model contains enumerations such as:
public enum MemberStatus {
ACTIVE,
SUSPENDED,
EXPIRED
}
JPA can store an enum in two ways:
@Enumerated(EnumType.ORDINAL)
or:
@Enumerated(EnumType.STRING)
We will use STRING.
@Enumerated(EnumType.STRING)
private MemberStatus status;
Why STRING?
Suppose the enum is:
ACTIVE,
SUSPENDED,
EXPIRED
With ORDINAL, the database might contain:
0
1
2
If the order of the enum changes later, existing database data can become incorrect.
With STRING, the database contains:
ACTIVE
SUSPENDED
EXPIRED
@Enumerated(EnumType.STRING)
unless you have a specific reason to do otherwise.
Part 5 : Repository Layers
DDD and Spring Data use the word repository in related but different ways. The domain layer should not need to know that we are using Spring Data JPA.
Domain Repository
Package:
membermanagement.infrastructurelayer
public interface MemberRepository extends JpaRepository <Member, Integer>
{
}
This code defines the repository for the Member Entity. It exprends the JpaRepository and hence inherits the basic CRUD operations. Notice how we must specify the entity type (Member) and also the type of the primary key for that entity (Integer). This is done between the triangle brackets <Member, Integer>.
This interface expresses what the domain needs. It does not mention any specific database technology. With this you will be able to get basic CRUD operations.
Domain | v Infrastructure | | MemberRepository v Spring Data JPA | v Hibernate | v H2
Part 6 : Member Management context Complete Example
At this stage, we have implemented the Member Management context with all its components: entities, value objects, and enumerations. Let us test the implementation by running the application and checking the database schema generated by Hibernate. As exlained in the previous sections, the files should be placed under the domainlayer and infrastructurelayer packages of the membermanagement bounded context package. Also make sure to have the following enumerated types defined:
public enum MembershipTier {
STANDARD,
PREMIUM
}
public enum MemberStatus {
ACTIVE,
SUSPENDED,
EXPIRED
}
The following image shows the file structure of the membermanagement bounded context package.
Part 7: Set up H2 database
In order to use H2 database you need to add the necessary dependency:
// For H2 Database
runtimeOnly 'com.h2database:h2'
implementation 'org.springframework.boot:spring-boot-h2console'
You also need to add configuration parameters in the application.properties file
# application.properties
spring.datasource.url=jdbc:h2:mem:librarydb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
# Enable the H2 web console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
Part 8 : Run the Application
At this point, you are ready to test the application using Intellij or ./gradlew bootrun command.
Part 9: Verify the Database Using H2 Console
This is the final step of Activity 03. We are not inserting any data yet. Instead, we want to verify that Hibernate has converted our Java model into a database schema.
Step 1 : Open H2 Console
Open:
http://localhost:8080/h2-console
Use the JDBC URL configured by your project. For example:
jdbc:h2:mem:librarydb
Use the username and password configured in
application.properties or
application.yml.
Step 2 : Connect
Click Connect.
Step 3 : Inspect the Tables
Look at the list of tables generated by Hibernate.
You should see table "members" corresponding to your persistent entity we just created.
The exact table and column names depend on your JPA mappings and configuration.
Step 4 : Inspect MEMBERS
Open the MEMBERS table.
Verify that columns corresponding to the entity and its
embedded Value Objects exist.
For example:
LIBRARY_ID
FIRSTNAME
LAST_NAME
EMAIL
MEMBERSHIP_TIER
MEMBERSHIP_STATUS
ACTIVE_LOAN_COUNT
REGISTRATION_DATE
The following figure shows the expected h2-console interface.
Part 10: Lombok and JPA: Important Pitfalls
Lombok is convenient, but convenience should not replace understanding.
Why not simply use @Data?
Consider:
@Entity
@Data
public class Member {
...
}
@Data generates several methods automatically.
Some of these methods can be inappropriate for JPA entities.
- Generated setters may allow uncontrolled state changes.
- Generated equality may not match entity identity semantics.
toString()can become problematic when entities contain relationships.- Generated methods can interact poorly with lazy-loaded associations.
Therefore, in this course we use a more controlled pattern:
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
...
}
Part 11 : Check Your Understanding
Before leaving the class, answer the following questions.
-
What does
@Entitytell Hibernate? -
What is the purpose of
@Id? -
Why does
Emailuse@Embeddableinstead of@Entity? -
Why are we using
@Enumerated(EnumType.STRING)? -
Why should a Loan contain
borrowerIdinstead of a directMemberobject? - What is the main advantage of using Lombok?
-
Why are we not simply using
@Dataon every JPA entity?
Troubleshooting
Problem: Lombok methods are not recognized
Check that Lombok is included in pom.xml
and that annotation processing is enabled in your IDE.
Problem: Hibernate says there is no default constructor
JPA requires an accessible no-argument constructor. Check for:
@NoArgsConstructor(access = AccessLevel.PROTECTED)
Problem: H2 Console does not open
Make sure the Spring Boot application is running and that the H2 Console has been enabled in your configuration.
Problem: A table is missing
Check that the class has @Entity and that the
package is included in Spring Boot's entity scanning.
Problem: Embedded fields have unexpected column names
Use @Column(name = "...") inside the
Value Object when you need to control the database column name.
Problem: Hibernate reports duplicate column names
Two embedded Value Objects may contain fields that map to
the same column name. Use explicit @Column
names to resolve the collision.
Reflection
In this activity, we moved from:
DDD Model
↓
Java Classes
↓
JPA Mapping
↓
Hibernate
↓
Database Tables
Consider the following question:
Does JPA determine how your domain should be designed, or does JPA simply provide a way to persist the model you designed?
Another important question:
Lombok can eliminate hundreds of lines of boilerplate code.
Should developers always use @Data?
Explain why or why not, particularly for JPA entities.
Up Next : Activity 04
In Activity 03, we created the structure of our persistent domain model.
In Activity 04, we will work with actual persistent data.
Activity 03 Persist the Model ↓ Tables Created ↓ Activity 04 Work With Data ↓ Create Read Update Delete ↓ Repositories
We will learn how to use repositories to save and retrieve domain objects, initialize test data, and perform basic persistence operations.
Appendix A : DDD to JPA Mapping
| DDD Concept | JPA Representation |
|---|---|
| Entity | @Entity |
| Aggregate Root | Usually a JPA @Entity |
| Identity | @Id |
| Value Object | @Embeddable |
| Embedded Value Object | @Embedded |
| Enumeration | @Enumerated(EnumType.STRING) |
| Domain Repository | Domain-level repository interface |
| Persistence Repository | JpaRepository |
| Inheritance | @Inheritance |
| Inheritance discriminator | @DiscriminatorColumn |
| Aggregate boundary | Domain design; not automatically enforced by JPA |
Appendix B : JPA Annotation Quick Reference
@Entity
→ Persistent class
@Table
→ Database table configuration
@Id
→ Primary key
@Embedded
→ Embed a Value Object
@Embeddable
→ Define a Value Object that can be embedded
@Column
→ Configure a database column
@Enumerated(EnumType.STRING)
→ Store enum names
@Inheritance
→ Configure entity inheritance
@DiscriminatorColumn
→ Identify subclass in SINGLE_TABLE inheritance
@DiscriminatorValue
→ Value identifying a particular subclass